// biome-ignore assist/source/organizeImports: import mocks first
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {flushPromises} from "../mocks/utils";
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman";

import stringify from "json-stable-stringify-without-jsonify";
import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store";
import {Controller} from "../../lib/controller";
import * as settings from "../../lib/util/settings";

returnDevices.push(
    devices.coordinator.ieeeAddr,
    devices.bulb_color.ieeeAddr,
    devices.bulb.ieeeAddr,
    devices.QBKG03LM.ieeeAddr,
    devices.bulb_color_2.ieeeAddr,
    devices.bulb_2.ieeeAddr,
    devices.GLEDOPTO_2ID.ieeeAddr,
    devices.InovelliVZM31SN.ieeeAddr,
);

describe("Extension: Groups", () => {
    let controller: Controller;

    beforeAll(async () => {
        vi.useFakeTimers();
        controller = new Controller(vi.fn(), vi.fn());
        await controller.start();
        await flushPromises();
    });

    afterAll(async () => {
        await controller?.stop();
        await flushPromises();
        vi.useRealTimers();
    });

    beforeEach(() => {
        resetGroupMembers();
        data.writeDefaultConfiguration();
        settings.reRead();
        mockMQTTPublishAsync.mockClear();
        groups.gledopto_group.command.mockClear();
        zhcGlobalStore.clear();
        controller.state.clear();
    });

    it("Should publish group state change when a device in it changes state", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);

        mockMQTTPublishAsync.mockClear();
        const payload = {data: {onOff: 1}, cluster: "genOnOff", device, endpoint, type: "attributeReport", linkquality: 10};
        await mockZHEvents.message(payload);
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should not republish identical optimistic group states", async () => {
        const device1 = devices.bulb_2;
        const device2 = devices.bulb_color_2;

        mockMQTTPublishAsync.mockClear();
        await mockZHEvents.message({
            data: {onOff: 1},
            cluster: "genOnOff",
            device: device1,
            endpoint: device1.getEndpoint(1),
            type: "attributeReport",
            linkquality: 10,
        });
        await mockZHEvents.message({
            data: {onOff: 1},
            cluster: "genOnOff",
            device: device2,
            endpoint: device2.getEndpoint(1),
            type: "attributeReport",
            linkquality: 10,
        });
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(6);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_tradfri_remote", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_2", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color_2", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_with_tradfri", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/ha_discovery_group", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/switch_group", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should publish state change of all members when a group changes its state", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should not publish state change when group changes state and device is disabled", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        settings.set(["devices", device.ieeeAddr, "disabled"], true);

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should publish state change for group when members state change", async () => {
        // Created for https://github.com/Koenkk/zigbee2mqtt/issues/5725
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should publish state of device with endpoint name", async () => {
        const group = groups.gledopto_group;

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/gledopto_group/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({state_cct: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(group.command).toHaveBeenCalledTimes(1);
        expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}, {});
    });

    it("Should publish state of group when specific state of specific endpoint is changed", async () => {
        const group = groups.gledopto_group;

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/GLEDOPTO_2ID/set", stringify({state_cct: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/GLEDOPTO_2ID", stringify({state_cct: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/gledopto_group", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(group.command).toHaveBeenCalledTimes(0);
    });

    it("Should publish state change of all members when a group changes its state, filtered", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        settings.set(["groups"], {1: {friendly_name: "group_1", retain: false, filtered_attributes: ["brightness"]}});

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON", brightness: 100}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON", brightness: 100}), {
            retain: false,
            qos: 0,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Shouldnt publish group state change when a group is not optimistic", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        settings.set(["groups"], {1: {friendly_name: "group_1", optimistic: false, retain: false}});

        mockMQTTPublishAsync.mockClear();
        const payload = {data: {onOff: 1}, cluster: "genOnOff", device, endpoint, type: "attributeReport", linkquality: 10};
        await mockZHEvents.message(payload);
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should publish state change of another group with shared device when a group changes its state", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        groups.group_1.members.push(endpoint);
        groups.group_2.members.push(endpoint);

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "ON"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({state: "ON"}), {retain: false, qos: 0});
    });

    it("Should not publish state change off if any lights within are still on when changed via device", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should publish state change off if any lights within are still on when changed via device when off_state: last_member_state is used", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false, off_state: "last_member_state"},
        });

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(1, "zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(2, "zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should not publish state change off if any lights within with non default-ep are still on when changed via device", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.QBKG03LM;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(2)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should not publish state change off if any lights within are still on when changed via device with non default-ep", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.QBKG03LM;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(2)!;
        const endpoint_3 = device_2.getEndpoint(3)!;
        endpoint_3.removeFromGroup(groups.ha_discovery_group);
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        group.members.push(endpoint_3);

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF", state_right: "ON"}), {
            retain: false,
            qos: 0,
        });
    });

    it("Should publish state change off if all lights within turn off with non default-ep", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.QBKG03LM;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(2)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false},
        });

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should publish state change off if all lights within turn off with non default-ep, but device state does not use them", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.InovelliVZM31SN;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(2)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false},
        });

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await mockMQTTEvents.message("zigbee2mqtt/wall_switch_double/set", stringify({state_left: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/wall_switch_double", stringify({state_left: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should not publish state change off if any lights within are still on when changed via shared group", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        groups.group_1.members.push(endpoint_1);
        groups.group_1.members.push(endpoint_2);
        groups.group_2.members.push(endpoint_1);

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/group_2/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_2", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should publish state change off if all lights within turn off", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false},
        });

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "OFF"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({state: "OFF"}), {retain: true, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Should only update group state with changed properties", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false},
        });
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF", color_temp: 200}));
        await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "ON", color_temp: 250}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 300}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bulb_color",
            stringify({color_mode: "color_temp", color_temp: 300, state: "OFF"}),
            {retain: false, qos: 0},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 300, state: "ON"}), {
            retain: true,
            qos: 0,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/group_1",
            stringify({color_mode: "color_temp", color_temp: 300, state: "ON"}),
            {retain: false, qos: 0},
        );
    });

    it("Should publish state change off even when missing current state", async () => {
        const device_1 = devices.bulb_color;
        const device_2 = devices.bulb;
        const endpoint_1 = device_1.getEndpoint(1)!;
        const endpoint_2 = device_2.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint_1);
        group.members.push(endpoint_2);
        settings.set(["groups"], {
            1: {friendly_name: "group_1", retain: false},
        });

        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({state: "ON"}));
        await flushPromises();
        mockMQTTPublishAsync.mockClear();
        controller.state.clear();

        await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({state: "OFF"}));
        await flushPromises();

        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({state: "OFF"}), {retain: false, qos: 0});
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({state: "OFF"}), {retain: false, qos: 0});
    });

    it("Add to group via MQTT", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1);
        const group = groups.group_1;
        expect(group.members.length).toBe(0);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/add",
            stringify({transaction: "123", group: "group_1", device: "bulb_color"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([endpoint]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, transaction: "123", status: "ok"}),
            {},
        );
    });

    it("Add to group via MQTT fails", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        expect(group.members.length).toBe(0);
        endpoint.addToGroup.mockImplementationOnce(() => {
            throw new Error("timeout");
        });
        await flushPromises();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", device: "bulb_color"}));
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {}, status: "error", error: "Failed to add to group (timeout)"}),
            {},
        );
    });

    it("Add to group with slashes via MQTT", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1);
        const group = groups["group/with/slashes"];
        settings.set(["groups"], {99: {friendly_name: "group/with/slashes", retain: false}});
        expect(group.members.length).toBe(0);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group/with/slashes", device: "bulb_color"}));
        await flushPromises();
        expect(group.members).toStrictEqual([endpoint]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {device: "bulb_color", endpoint: "default", group: "group/with/slashes"}, status: "ok"}),
            {},
        );
    });

    it("Add to group via MQTT with postfix", async () => {
        const device = devices.QBKG03LM;
        const endpoint = device.getEndpoint(3)!;
        const group = groups.group_1;
        expect(group.members.length).toBe(0);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/add",
            stringify({group: "group_1", device: "wall_switch_double", endpoint: "right"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([endpoint]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Add to group via MQTT with postfix shouldnt add it twice", async () => {
        const device = devices.QBKG03LM;
        const endpoint = device.getEndpoint(3)!;
        const group = groups.group_1;
        expect(group.members.length).toBe(0);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/add",
            stringify({group: "group_1", device: "wall_switch_double", endpoint: "right"}),
        );
        await flushPromises();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/add",
            stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "3"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([endpoint]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group via MQTT", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group via MQTT fails", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        endpoint.removeFromGroup.mockImplementationOnce(() => {
            throw new Error("timeout");
        });
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1", device: "bulb_color"}));
        await flushPromises();
        expect(group.members.length).toStrictEqual(1);
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {}, status: "error", error: "Failed to remove from group (timeout)"}),
            {},
        );
    });

    it("Remove from group via MQTT keeping device reporting", async () => {
        const device = devices.bulb_color;
        const endpoint = device.getEndpoint(1)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/remove",
            stringify({group: "group_1", device: "bulb_color", skip_disable_reporting: true}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group via MQTT with postfix variant 1", async () => {
        const device = devices.QBKG03LM;
        const endpoint = device.getEndpoint(3)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/remove",
            stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "3"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {device: "0x0017880104e45542", endpoint: "3", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group via MQTT with postfix variant 2", async () => {
        const device = devices.QBKG03LM;
        const endpoint = device.getEndpoint(3)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/remove",
            stringify({group: "group_1", device: "wall_switch_double", endpoint: "3"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {device: "wall_switch_double", endpoint: "3", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group via MQTT with postfix variant 3", async () => {
        const device = devices.QBKG03LM;
        const endpoint = device.getEndpoint(3)!;
        const group = groups.group_1;
        group.members.push(endpoint);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/remove",
            stringify({group: "group_1", device: "0x0017880104e45542", endpoint: "right"}),
        );
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {device: "0x0017880104e45542", endpoint: "right", group: "group_1"}, status: "ok"}),
            {},
        );
    });

    it("Remove from group all", async () => {
        const group = groups.group_1;
        groups.group_1.members.push(devices.QBKG03LM.endpoints[2]);
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove_all", stringify({device: "0x0017880104e45542", endpoint: "right"}));
        await flushPromises();
        expect(group.members).toStrictEqual([]);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove_all",
            stringify({data: {device: "0x0017880104e45542", endpoint: "right"}, status: "ok"}),
            {},
        );
    });

    it("Error when adding to non-existing group", async () => {
        mockLogger.error.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/remove", stringify({group: "group_1_not_existing", device: "bulb_color"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/remove",
            stringify({data: {}, status: "error", error: "Group 'group_1_not_existing' does not exist"}),
            {},
        );
    });

    it("Error when adding a non-existing device", async () => {
        mockLogger.error.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", device: "bulb_color_not_existing"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {}, status: "error", error: "Device 'bulb_color_not_existing' does not exist"}),
            {},
        );
    });

    it("Error when adding a non-existing endpoint", async () => {
        mockLogger.error.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message(
            "zigbee2mqtt/bridge/request/group/members/add",
            stringify({group: "group_1", device: "bulb_color", endpoint: "not_existing_endpoint"}),
        );
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {}, status: "error", error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
            {},
        );
    });

    it("Error when invalid payload", async () => {
        mockLogger.error.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({group: "group_1", devicez: "bulb_color"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Error when add/remove with invalid payload", async () => {
        mockLogger.error.mockClear();
        mockMQTTPublishAsync.mockClear();
        mockMQTTEvents.message("zigbee2mqtt/bridge/request/group/members/add", stringify({groupz: "group_1", device: "bulb_color"}));
        await flushPromises();
        expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), expect.any(Object));
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bridge/response/group/members/add",
            stringify({data: {}, status: "error", error: "Invalid payload"}),
            {},
        );
    });

    it("Should only include relevant properties when publishing member states", async () => {
        const bulbColor = devices.bulb_color;
        const bulbColorTemp = devices.bulb;
        const group = groups.group_1;
        group.members.push(bulbColor.getEndpoint(1)!);
        group.members.push(bulbColorTemp.getEndpoint(1)!);

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color_temp: 50}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_color", stringify({color_mode: "color_temp", color_temp: 50}), {
            retain: false,
            qos: 0,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/group_1", stringify({color_mode: "color_temp", color_temp: 50}), {
            retain: false,
            qos: 0,
        });
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 50}), {
            retain: true,
            qos: 0,
        });

        mockMQTTPublishAsync.mockClear();
        await mockMQTTEvents.message("zigbee2mqtt/group_1/set", stringify({color: {x: 0.5, y: 0.3}}));
        await flushPromises();
        expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/bulb_color",
            stringify({color: {x: 0.5, y: 0.3}, color_mode: "xy", color_temp: 548}),
            {retain: false, qos: 0},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
            "zigbee2mqtt/group_1",
            stringify({color: {x: 0.5, y: 0.3}, color_mode: "xy", color_temp: 548}),
            {retain: false, qos: 0},
        );
        expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({color_mode: "color_temp", color_temp: 548}), {
            retain: true,
            qos: 0,
        });
    });
});
